# This file is part of Howlr.
#
# Howlr is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Howlr is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Howlr.  If not, see <http://www.gnu.org/licenses/>.

require 'howlr/deliverers'
require 'restr'

class Howlr::Message
  
  attr_accessor :from, :subject, :body, :author, :content_type
  attr_accessor :callback_url, :callback_method, 
                :callback_username, :callback_password
  attr_reader :recipients, :id, :sent, :delivery_errors
  
  # Initializes a new message with the given data.
  # <tt>params</tt> should be a hash with values for the various Message attributes.
  def initialize(data)
    self.from       = data[:from]
    self.subject    = data[:subject]
    self.body       = data[:body]
    self.author     = data[:author]
    self.content_type = data[:content_type] || 'text/plain'
    
    self.callback_url      = data[:callback_url]
    self.callback_method   = data[:callback_method]
    self.callback_username = data[:callback_username]
    self.callback_password = data[:callback_password]
    
    @recipients     = parse_recipients(data[:recipients])
    @id = "#{Time.now.to_i}#{rand(9999999)}".to_i
    @delivery_errors = []
    @sent = nil
  end
  
  def parse_recipients(raw_recipients)
    $LOG.debug "Parsing recipients: #{raw_recipients.inspect}"
    
    raw_recipients = raw_recipients.split(/[,;]/) if raw_recipients.kind_of?(String)
    recipients = []
    raw_recipients.each do |raw|
      recipients << Recipient.parse(raw)
    end
    
    return recipients
  end
  
  # Delivers the message to each recipient, stores the result of the overall delivery
  # in @sent, and triggers the callback if a callback_url is present.
  #
  # After running this, @sent will contain one of the following values:
  # nil :: Nothing was not sent (the recipients list is probably empty)
  # Time :: The message was sent successfully to all recipients at this time
  # false :: There was a problem sending the message to at least one recipient.
  #          Check delivery_errors for more information about what caused the failure.
  def send
    recipients.each do |r|
      next if r.nil?
      
      msg = self.dup
      msg.instance_variable_set(:@recipients, [r])
      
      $LOG.debug "Sending: #{msg.inspect}"
      
      result = r.deliverer.deliver(msg)
      @sent = Time.now.to_s if (result && (@sent || @sent.nil?))
      
      begin
      callback(r, result) unless callback_url.nil? || callback_url.blank?
      rescue => e
        $LOG.error("Error during callback: #{e}")
        e2 = CallbackError.new("Error during callback: #{e}")
        e2.underlying_error = e
        raise e2
      end
    end
  end
  
  # If the message has a callback_url, this callback method is triggered
  # for each delivery attempt (i.e. for every recipient, when #send is 
  # called).
  #
  # The callback sends a REST request to the given callback_url with
  # the given callback_method, submitting information about the result
  # of the delivery attempt. Have a look at this method's body for
  # information about what data is submitted with the request. 
  def callback(recipient, result)
    auth = {}
    auth[:username] = callback_username if callback_username
    auth[:password] = callback_password if callback_password
    
    data = {}
    data[:message_id] = id
    data[:recipient_address] = recipient.address
    data[:recipient_protocol] = recipient.deliverer.protocol
    data[:send_success] = result
    data[:send_datetime] = sent
    data[:send_timestamp] = sent.to_i if sent.kind_of? Time
    data[:delivery_errors] = delivery_errors.join("; ")
    
    Restr.do(callback_method, callback_url, data, auth)
  end
  
  # Serializes this Message into XML and returns a Builder::XmlMarkup
  # object.
  #
  # You should be able to call #to_s on the returned Builder::XmlMarkup
  # object to get the XML as a String.
  def to_xml(options = {})
    xml = options[:builder] ||= Builder::XmlMarkup.new(options)
    xml.instruct! unless options[:skip_instruct]
    
    xml.message(id ? {:id => id} : {}) do
      xml.recipients do
        recipients.each{|r| r.to_xml(options)}
      end
      
      h = {:from => from, :subject => subject, :author => author}
      h.each do |k,v|
        xml.tag!(k, v)
      end
      
      xml.sent sent
      
      unless sent.nil?
        xml.delivery_errors do
          delivery_errors.each do |err|
            xml.delivery_error err
          end
        end
      end
      
      xml.body {xml.cdata! body}
    end
  end
  
  
  class Recipient
    
    attr_accessor :deliverer, :address
    
    def initialize(deliverer, address)
      @deliverer = deliverer
      @address = address
    end
    
    
    def self.parse(raw)
      return nil if raw.nil?
      
      raw =~ /^(?:(\w*):)?(.*)/
      
      if $~[1]
        protocol = $~[1].intern
        address = $~[2]
      elsif $~
        protocol = :mailto
        address = raw
      else
        raise ArgumentError, "#{raw.inspect} is not a valid address."
      end
      
      $LOG.debug "Parsing raw recipient: #{raw.inspect} --> protocol: #{protocol.inspect}, address: #{address.inspect}"
      
      case protocol
      when :mailto
        deliverer = Howlr::Deliverers::Email
      else
        raise ArgumentError, "#{protocol.inspect} is not a valid delivery protocol!"
      end
      
      return Recipient.new(deliverer, address)
    end
    
    def to_s
      "#{deliverer.protocol}:#{address}"
    end
    
    def to_xml(options = {})
      if b = options[:builder]
        b.recipient to_s
      else
        "<recipient>#{to_s}</recipient>"
      end
    end
  end
  
  class CallbackError < Exception
    attr_accessor :underlying_error
  end
end